﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ServerUtils.Test
{
    [TestClass]
    public class MRInvokerTest
    {
        private MRInvoker target;
        private int maxRetry = 4;
        private int retryInterval = 100;
        private List<InvokerErrorEventArgs> errors;
        private bool handleException;

        [TestInitialize]
        public void Initialize()
        {
            this.target = new MRInvoker(this.retryInterval, this.maxRetry);
            this.target.Error += this.TargetError;

            this.errors = new List<InvokerErrorEventArgs>();
            this.handleException = true;
        }

        #region Invoke Action

        [TestMethod]
        public void InvokeAction_GivenAction_ActionIsCalled()
        {
            int count = 0;
            this.target.Invoke(() => { count++; });

            Assert.AreEqual(1, count);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInAction_ActionIsCalledMoreThanOnce()
        {
            int count = 0;
            this.target.Invoke(() => 
            { 
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }                
            });

            Assert.AreEqual(3, count);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInAction_ActionIsCalledAfterRetryInterval()
        {
            int count = 0;
            Stopwatch sw = new Stopwatch();
            this.target.Invoke(() =>
            {
                count++;
                if (count < 2)
                {
                    sw.Start();
                    throw new Exception(count.ToString());
                }

                sw.Stop();
            });

            Assert.IsTrue(sw.ElapsedMilliseconds + 1 > this.retryInterval);
            Assert.IsTrue(sw.ElapsedMilliseconds < 1.5 * this.retryInterval);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInAction_ErrorEventIsRaised()
        {
            int count = 0;
            this.target.Invoke(() =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }
            });

            Assert.AreEqual(2, this.errors.Count);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInAction_ActionIsCalledMaxRetryTimes()
        {
            int count = 0;

            try
            {
                this.target.Invoke(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, count);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInActionAllTheTimes_ExceptionIsRaised()
        {
            int count = 0;

            try
            {
                this.target.Invoke(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(this.maxRetry, count);
                Assert.AreEqual("Maximum number of retries", ex.Message);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInActionAllTheTimes_ErrorEventIsRaised()
        {
            int count = 0;
            var exception = new Exception(count.ToString());

            try
            {
                this.target.Invoke(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.errors.Count);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 1), this.errors[0]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 2), this.errors[1]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 3), this.errors[2]);
        }

        [TestMethod]
        public void InvokeAction_GivenUnhandledExceptionInAction_ExceptionIsRaised()
        {
            int count = 0;
            this.handleException = false;
            var exception = new Exception();

            try
            {
                this.target.Invoke(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(1, count);
                Assert.AreSame(exception, ex);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        #endregion

        #region Invoke Function

        [TestMethod]
        public void InvokeFunction_GivenFunction_FunctionIsCalled()
        {
            int count = 0;
            this.target.Invoke<int>(() => { return count++; });

            Assert.AreEqual(1, count);
        }

        [TestMethod]
        public void InvokeFunction_GivenFunction_FunctionResultIsReturned()
        {
            object result = new object();
            var actual = this.target.Invoke<object>(() => { return result; });

            Assert.AreSame(result, actual);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunction_FunctionIsCalledMoreThanOnce()
        {
            object result = new object();
            int count = 0;
            this.target.Invoke<object>(() =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                return result;
            });

            Assert.AreEqual(3, count);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunction_FunctionIsCalledAfterRetryInterval()
        {
            object result = new object();
            int count = 0;
            Stopwatch sw = new Stopwatch();
            this.target.Invoke<object>(() =>
            {
                count++;
                if (count < 2)
                {
                    sw.Start();
                    throw new Exception(count.ToString());
                }

                sw.Stop();
                return result;
            });

            Assert.IsTrue(sw.ElapsedMilliseconds + 1 > this.retryInterval);
            Assert.IsTrue(sw.ElapsedMilliseconds < 1.5 * this.retryInterval);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunction_ErrorEventIsRaised()
        {
            object result = new object();
            int count = 0;
            this.target.Invoke<object>(() =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                return result;
            });

            Assert.AreEqual(2, this.errors.Count);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunction_FuctionIsCalledMaxRetryTimes()
        {
            int count = 0;

            try
            {
                this.target.Invoke<object>(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, count);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunctionAllTheTimes_ExceptionIsRaised()
        {
            int count = 0;

            try
            {
                this.target.Invoke<object>(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(this.maxRetry, count);
                Assert.AreEqual("Maximum number of retries", ex.Message);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunctionAllTheTimes_ErrorEventIsRaised()
        {
            int count = 0;
            var exception = new Exception(count.ToString());

            try
            {
                this.target.Invoke<object>(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.errors.Count);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 1), this.errors[0]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 2), this.errors[1]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 3), this.errors[2]);
        }

        [TestMethod]
        public void InvokeFunction_GivenUnhandledExceptionInFuction_ExceptionIsRaised()
        {
            int count = 0;
            this.handleException = false;
            var exception = new Exception();

            try
            {
                this.target.Invoke<object>(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(1, count);
                Assert.AreSame(exception, ex);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        #endregion

        #region Invoke Async Action

        [TestMethod]
        public async Task InvokeAsyncAction_GivenAsyncAction_AsyncActionIsCalled()
        {
            int count = 0;
            await this.target.InvokeAsync(async () =>
            {
                count++;
                await Task.Delay(1);
            });            

            Assert.AreEqual(1, count);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncAction_AsyncActionIsCalledMoreThanOnce()
        {
            int count = 0;
            await this.target.InvokeAsync(async () =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                await Task.Delay(1);
            });

            Assert.AreEqual(3, count);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncAction_AsyncActionIsCalledAfterRetryInterval()
        {
            int count = 0;
            Stopwatch sw = new Stopwatch();
            await this.target.InvokeAsync(async () =>
            {
                count++;
                if (count < 2)
                {
                    sw.Start();
                    throw new Exception(count.ToString());
                }

                sw.Stop();
                await Task.Delay(1);
            });

            Assert.IsTrue(sw.ElapsedMilliseconds + 1 > this.retryInterval);
            Assert.IsTrue(sw.ElapsedMilliseconds < 1.5 * this.retryInterval);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncAction_ErrorEventIsRaised()
        {
            int count = 0;
            await this.target.InvokeAsync(async () =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                await Task.Delay(1);
            });

            Assert.AreEqual(2, this.errors.Count);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncAction_AsyncActionIsCalledMaxRetryTimes()
        {
            int count = 0;

            try
            {
                await this.target.InvokeAsync(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, count);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncActionAllTheTimes_ExceptionIsRaised()
        {
            int count = 0;

            try
            {
                await this.target.InvokeAsync(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(this.maxRetry, count);
                Assert.AreEqual("Maximum number of retries", ex.Message);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAsyncActionAllTheTimes_ErrorEventIsRaised()
        {
            int count = 0;
            var exception = new Exception(count.ToString());

            try
            {
                await this.target.InvokeAsync(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.errors.Count);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 1), this.errors[0]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 2), this.errors[1]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 3), this.errors[2]);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenUnhandledExceptionInAsyncAction_ExceptionIsRaised()
        {
            int count = 0;
            this.handleException = false;
            var exception = new Exception();

            try
            {
                await this.target.InvokeAsync(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(1, count);
                Assert.AreSame(exception, ex);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        #endregion

        #region Invoke Async Function

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenAsyncFunction_FunctionIsCalled()
        {
            int count = 0;
            await this.target.InvokeAsync<int>(async () => 
            {
                return await Task<int>.Run(() => { return count++; }); 
            });

            Assert.AreEqual(1, count);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenAsyncFunction_FunctionResultIsReturned()
        {
            object result = new object();
            var actual = await this.target.InvokeAsync<object>(async () => 
            {
                return await Task<object>.Run(() => { return result; });
            });

            Assert.AreSame(result, actual);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunction_FunctionIsCalledMoreThanOnce()
        {
            object result = new object();
            int count = 0;
            await this.target.InvokeAsync<object>(async () =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                return await Task<object>.Run(() => { return result; });
            });

            Assert.AreEqual(3, count);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunction_FunctionIsCalledAfterRetryInterval()
        {
            object result = new object();
            int count = 0;
            Stopwatch sw = new Stopwatch();
            await this.target.InvokeAsync<object>(async () =>
            {
                count++;
                if (count < 2)
                {
                    sw.Start();
                    throw new Exception(count.ToString());
                }

                sw.Stop();
                return await Task<object>.Run(() => { return result; });
            });

            Assert.IsTrue(sw.ElapsedMilliseconds + 1 > this.retryInterval);
            Assert.IsTrue(sw.ElapsedMilliseconds < 1.5 * this.retryInterval);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunction_ErrorEventIsRaised()
        {
            object result = new object();
            int count = 0;
            await this.target.InvokeAsync<object>(async () =>
            {
                count++;
                if (count < 3)
                {
                    throw new Exception(count.ToString());
                }

                return await Task<object>.Run(() => { return result; });
            });

            Assert.AreEqual(2, this.errors.Count);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunction_AsyncFuctionIsCalledMaxRetryTimes()
        {
            int count = 0;

            try
            {
                await this.target.InvokeAsync<object>(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, count);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunctionAllTheTimes_ExceptionIsRaised()
        {
            int count = 0;

            try
            {
                await this.target.InvokeAsync<object>(() =>
                {
                    count++;
                    throw new Exception(count.ToString());
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(this.maxRetry, count);
                Assert.AreEqual("Maximum number of retries", ex.Message);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInAsyncFunctionAllTheTimes_ErrorEventIsRaised()
        {
            int count = 0;
            var exception = new Exception(count.ToString());

            try
            {
                await this.target.InvokeAsync<object>(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.errors.Count);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 1), this.errors[0]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 2), this.errors[1]);
            this.CompareInvokerErrorEventArgs(new InvokerErrorEventArgs(exception, 3), this.errors[2]);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenUnhandledExceptionInAsyncFuction_ExceptionIsRaised()
        {
            int count = 0;
            this.handleException = false;
            var exception = new Exception();

            try
            {
                await this.target.InvokeAsync<object>(() =>
                {
                    count++;
                    throw exception;
                });
            }
            catch (Exception ex)
            {
                Assert.AreEqual(1, count);
                Assert.AreSame(exception, ex);
                return;
            }

            Assert.Fail("an expected exception was not raised.");
        }

        #endregion

        private void CompareInvokerErrorEventArgs(InvokerErrorEventArgs expected, InvokerErrorEventArgs actual)
        {
            Assert.AreSame(expected.Exception, actual.Exception);
            Assert.AreEqual(expected.ExceptionCount, actual.ExceptionCount);
        }

        private void TargetError(object sender, InvokerErrorEventArgs e)
        {
            e.ExceptionHandled = this.handleException;
            this.errors.Add(e);
        }
    }
}
